<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

/**
 * SpicaArrayList represents resizable-array implementation that provides object-oriented
 * API to work with an array.
 *
 * namespace spica\core\utils\ArrayList
 *
 * @category   spica
 * @package    core
 * @package    utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 08, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: ContainerUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaArrayList extends ArrayObject
{
    /**
     * Populates or replace the SpicaArrayList content with an array.
     *
     * @var array $array
     */
    public function populate($array)
    {
        $this->exchangeArray($array);
    }

    /**
     * Gets the value at $offset.
     * If the $offset does not exist /or/ the value at $offset is NULL, then the
     * $default is returned. The array is unaffected.
     *
     * @param  string $offset  Offset to retrieve
     * @param  mixed  $default Default value
     * @return mixed Value at offset, or NULL
     */
    public function get($offset, $default = null)
    {
        return $this->offsetExists($offset) ? parent::offsetGet($offset) : $default;
    }

    /**
     * Sets a value at $offset.
     *
     * @param  string $offset  Offset to modify or add
     * @param  mixed  $default Value to add
     */
    public function set($offset, $value)
    {
        parent::offsetSet($offset, $value);
    }

    /**
     * Tests if the specified $offset is a key in this collection.
     *
     * @param  string $offset Offset to check
     * @return bool Returns true if a value exists at $offset, false otherwise.
     */
    public function hasKey($offset)
    {
        return parent::offsetExists($offset);
    }

    /**
     * Gets an array containing the keys of this array.
     *
     * @return array Array of keys
     */
    public function getKeys()
    {
        $keys = array();

        foreach ($this->getIterator() as $k => $v)
        {
            $keys[] = $k;
        }

        return $keys;
    }

    /**
     * Gets an array containing the values of this array.
     *
     * @return array Array of values
     */
    public function getValues()
    {
        $values = array();

        foreach ($this->getIterator() as $k => $v)
        {
            $values[] = $v;
        }

        return $values;
    }

    /**
     * Updates the current data items with the contents of $array.
     *
     * @see    http://bugs.php.net/bug.php?id=28165 Non trivial bug
     * @param  string|int $element Offset or key name
     * @return mixed
     */
    public function indexOf($element)
    {
        return array_search($element, $this->getArrayCopy(), true);
    }

    /**
     * Updates the current data items with the contents of $array.
     *
     * @param array $array Array to merge into this one
     */
    public function update($array)
    {
        foreach ($array as $k => $v)
        {
            parent::offsetSet($k, $v);
        }
    }

    /**
     * Gets the value at $offset and deletes it from the array.
     *
     * If no value exists at $offset, or the value at $offset is NULL, then
     * the $default will be returned.
     *
     * @param  string $offset  Offset to pop
     * @param  string $default Default value
     * @return mixed Value at offset or default
     */
    public function pop($offset, $default = null)
    {
        if (true   === $this->offsetExists($offset))
        {
            $value = $this->offsetGet($offset);
            $this->offsetUnset($offset);
            return $value;
        }

        return $default;
    }

    /**
     * Removes data item at $offset and keep the current index.
     *
     * @return bool
     */
    public function remove($offset)
    {
        if (true === parent::offsetExists($offset))
        {
            return parent::offsetUnset($offset);
        }

        return false;
    }

    /**
     * Removes all the contained data items.
     */
    public function removeAll()
    {
        $this->exchangeArray(array());
    }

    /**
     * Checks if there is no data items contained in the list.
     *
     * @return bool
     */
    public function isEmpty()
    {
        return (0 === $this->count());
    }

    /**
     * Gets JSON representation of the internal array.
     *
     * @return string
     */
    public function toJson()
    {
        return json_encode($this->getArrayCopy());
    }

    /**
     * Converts this object to a string.
     *
     * @return string
     */
    public function __toString()
    {
        return spica_var_dump($this);
    }
}

/**
 * This is an abstract class which hides the details of the application specific
 * data sets from controller and view objects.
 *
 * namespace spica\core\utils\AbstractDataSet
 *
 * @category   spica
 * @package    core
 * @package    utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 08, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: ContainerUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
abstract class SpicaAbstractDataSet extends SpicaArrayList
{
    /**
     * An error message which indicates why this data set is not readable.
     *
     * @var string $message
     */
    protected $_message;

    /**
     * If this value is true, you can get the value stored in this object.
     * If it is false, the object is unreadable.
     *
     * @var bool
     */
    protected $_readable = true;

    /**
     * Sets a message which is displayed and indicates why this object is not readable.
     *
     * @param string $message
     */
    public function setMessage($message)
    {
        $this->_message = $message;
    }

    /**
     * Returns the message that is displayed and indicates why this object is not readable.
     *
     * @param  string $message
     * @return string A message, usually error message
     */
    public function getMessage()
    {
        return $this->_message;
    }

    /**
     * Sets a message to be displayed when there is no data in the dataset but
     * data set is not in errorous status.
     *
     * @param  string $message
     * @return bool   Return true if the message is set, false if it isn't
     */
    public function setMessageWhenEmpty($message)
    {
        if (true === $this->_readable && 0 === $this->count())
        {
            $this->_readable = false;
            $this->_message  = $message;
            return true;
        }

        return false;
    }

    /**
     * Checks if the data in this object can be read as normal.
     * If this method return false, it means that it can not be read due to an
     * error occurs. In that case we will use getMessage() to retrieve what happens
     *
     * @return bool
     */
    public function isReadable()
    {
        return $this->_readable;
    }

    /**
     * Sets readable status to the object.
     * If the status is set to be false, it implies that there is something
     * wrong with the query or database
     *
     * @param bool $readable
     */
    public function setReadable($readable)
    {
        $this->_readable = (bool) $readable;
    }
}

/**
 * A SpicaRowList is a collection of database records. Each record is an array
 * with a key representing row's field name and its associated value as the value
 * of the field at a given row. SpicaRowList can be used to represents a tabular data set.
 *
 * It works like SpicaArrayList, with all the functionality, but each iterable entry
 * in SpicaDataSet is a SpicaArrayList.
 *
 * Ported from Pone_DataSet (PONE framework)
 *
 * namespace spica\core\utils\RowList
 *
 * @category   spica
 * @package    core
 * @package    utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 08, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: ContainerUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaRowList extends SpicaAbstractDataSet
{
    /**
     * Error value used in a list/menu (html selection) when this SpicaRowList
     * is unreadable and developer choose to build an option with the label named
     * under getMessage(). This kind of label should be valued in this constant value
     */
    const ERROR_VALUE = '____ERROR___';

    /**
     * Indicate the total records found for one query to get all the data
     *
     * @var int Numeric string
     */
    private $_totalRecords;

    /**
     * Current page
     *
     * @var int Numeric string
     */
    private $_currentPage;

    /**
     * Total page
     *
     * @var string
     */
    private $_totalPage;

    /**
     * Constructs an object of <code>SpicaRowList</code>
     *
     * @param array $data
     */
    public function __construct(array $data = null)
    {
        if (null  === $data)
        {
            $data = array();
        }

        /**
         * @tutorial
         * @see Obsolete parent::__construct($data);
         * Constructor will populate the internal data as an array without change
         * setData will populate a new type of array in which each child array
         * will be an SpicaArrayList instance
         */
        $this->fill($data);
    }

    /**
     * Stores an array of data (a row or rows) as an internal array.
     *
     * @param array $data The array of rows/columns or a scalar value or an another data type value
     *                    retrieved from a datasource
     * @param bool $rowAsArrayObject If true then every rows will be wrapped by a SpicaArrayList
     */
    public function fill($data, $rowAsArrayObject = true)
    {
        if (null === $data)
        {
            return;
        }
        
        if (false === is_array($data))
        {
            return $this->exchangeArray(array(0 => $data));
        }

        if (true === $rowAsArrayObject)
        {
            foreach ($data as $index => $row)
            {
                if (true === is_array($row))
                {
                    $row = new SpicaArrayList($row);
                }

                $data[$index] = $row;
            }
        }

        return $this->exchangeArray($data);
    }

    /**
     * Retrieves a single scalar value stored in the result set object
     *
     * @return string A scalar value
     */
    public function value()
    {
        return $this->get(0);
    }

    /**
     * Sets the number of records for full data fetching
     * This method is used when we want to do paging.
     *
     * @see setData() is to set the data for each page. This method is to set
     *      the number of records when we try to fetch all the data in one page.
     * @param string $totalRecords Numeric string
     */
    public function setTotalRecords($totalRecords)
    {
        $this->_totalRecords = (int) $totalRecords;
    }

    /**
     * Gets the total records
     *
     * @return string Numeric string
     */
    public function getTotalRecords()
    {
        return $this->_totalRecords;
    }

    /**
     * Sets total page
     *
     * @param string $totalPage
     */
    public function setTotalPage($totalPage)
    {
        $this->_totalPage = $totalPage;
    }

    /**
     * Gets total page
     *
     * @return string Numeric string
     */
    public function getTotalPage()
    {
        return $this->_totalPage;
    }

    /**
     * Sets current page.
     *
     * @param string $pageNumber Numeric string
     */
    public function setCurrentPage($pageNumber)
    {
        $this->_currentPage = (int) $pageNumber;
    }

    /**
     * Gets the current page.
     *
     * @return int Numeric string
     */
    public function getCurrentPage()
    {
        return $this->_currentPage;
    }

    /**
     * Fetches a column values from a database result set.
     *
     * @param  string $columnName
     * @return SpicaRowList
     */
    public function fetchColumn($columnName = null)
    {
        if (null === $columnName)
        {
            throw new InvalidArgumentException('fetchColumn() requires a full-qualified column name');
        }

        $rs   = new SpicaRowList();
        $data = array();

        if (0 === $this->count())
        {
            return $rs;
        }

        if (true  === $this->hasKey($columnName))
        {
            $data = (array) $this->get($columnName);
            return new SpicaRowList($data);
        }

        $iterator = $this->getIterator();

        foreach ($iterator as $row)
        {
            if (true === isset($row[$columnName]))
            {
                $data[] = $row[$columnName];
            }
        }

        return new SpicaRowList($data);
    }

    /**
     * Gets conventional on error occurs.
     *
     * PHP 5.2.x does not support to access a constant from a variable.
     * This a workaround. In PHP 5.2.x: $object::CONSTANT_2 (fatal error) but in
     * PHP 5.3.x: $object::CONSTANT_2 (OK - late static binding and dynamic static binding)
     *
     * @return string
     */
    public function valueOnError()
    {
        return self::ERROR_VALUE;
    }
}

/**
 * A SpicaRowList is a collection of grouped database records.
 *
 * namespace spica\core\utils\RowGroup
 *
 * @category   spica
 * @package    core
 * @package    utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 10, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: ContainerUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaRowGroup extends SpicaRowList
{
    /**
     * Constructs an object of <code>SpicaRowGroup</code>
     *
     * @param array  $data
     * @param string $fieldToGroup Field name that is used to group related records
     * @param string $groupKey     Field that contains related records in resulted row group
     * @param array  $masterFields
     * @param array  $slaveFields
     */
    public function __construct($data, $fieldToGroup, $groupKey, $masterFields, $slaveFields)
    {
        $group = array();
        $index = array();

        foreach ($data as $row)
        {
            // If this company is not indexed yet
            if (!isset($index[$row[$fieldToGroup]]))
            {
                $field = array();

                foreach ($masterFields as $name)
                {
                    $field[$name] = $row[$name];
                }

                $group[] = $field;
                $index[$row[$fieldToGroup]] = count($group) - 1;
            }

            $field = array();
            $i     = $index[$row[$fieldToGroup]];

            foreach ($slaveFields as $name)
            {
                $field[$name] = $row[$name];
            }

            $group[$i][$groupKey][] = new SpicaArrayList($field);
        }

        $this->fill($group);
    }
}

?>